/*  Starshatter OpenSource Distribution
    Copyright (c) 1997-2004, Destroyer Studios LLC.
    All Rights Reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    * Neither the name "Destroyer Studios" nor the names of its contributors
      may be used to endorse or promote products derived from this software
      without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.

    SUBSYSTEM:    Stars.exe
    FILE:         CameraDirector.cpp
    AUTHOR:       John DiCamillo


    OVERVIEW
    ========
    Camera Director singleton manages the main view camera based on the current ship
*/

#include "MemDebug.h"
#include "CameraDirector.h"
#include "Ship.h"
#include "FlightDeck.h"
#include "Contact.h"
#include "Sim.h"
#include "StarSystem.h"
#include "Terrain.h"
#include "HUDView.h"
#include "DetailSet.h"
#include "Starshatter.h"

#include "Game.h"

// +----------------------------------------------------------------------+

CameraDirector*   CameraDirector::instance = 0;

static double     range_max_limit = 300e3;

// +----------------------------------------------------------------------+

CameraDirector*
CameraDirector::GetInstance()
{
    if (!instance)
    instance = new(__FILE__,__LINE__) CameraDirector;

    return instance;
}

// +----------------------------------------------------------------------+

CameraDirector::CameraDirector()
    : mode(MODE_COCKPIT), requested_mode(MODE_NONE), old_mode(MODE_NONE),
      sim(0), ship(0), region(0), external_ship(0), external_body(0),
      virt_az(0), virt_el(0), virt_x(0), virt_y(0), virt_z(0),
      azimuth(PI/4), elevation(PI/4), az_rate(0), el_rate(0), range_rate(0),
      range(0), range_min(100), range_max(range_max_limit),
      base_range(0), transition(0), hud(0)
{
    instance = this;
}

// +--------------------------------------------------------------------+

CameraDirector::~CameraDirector()
{
    if (instance == this)
    instance = 0;
}

// +--------------------------------------------------------------------+

void
CameraDirector::Reset()
{
    mode           = MODE_COCKPIT;
    requested_mode = MODE_NONE;
    old_mode       = MODE_NONE;

    sim            = 0;
    ship           = 0;
    region         = 0;
    external_ship  = 0;
    external_body  = 0;
    transition     = 0;
    hud            = 0;
}

// +--------------------------------------------------------------------+

void
CameraDirector::SetShip(Ship* s)
{
    sim = Sim::GetSim();
    hud = HUDView::GetInstance();

    // can't take control of a dead ship:
    if (s && (s->Life() == 0 || s->IsDying() || s->IsDead()))
    return;

    // leaving the old ship, so make sure it is visible:
    if (ship && ship != s) {
        ship->ShowRep();
        ship->HideCockpit();
    }

    // taking control of the new ship:
    ship = s;

    if (ship) {
        Observe(ship);
        region = ship->GetRegion();

        if (sim && ship->GetRegion() != sim->GetActiveRegion())
        sim->ActivateRegion(ship->GetRegion());

        range  = ship->Radius() * 4;

        if (mode == MODE_COCKPIT)
        mode = MODE_CHASE;

        SetMode(MODE_COCKPIT);
        ExecFrame(0);
    }
}

// +--------------------------------------------------------------------+

void
CameraDirector::SetMode(int m, double t)
{
    if (requested_mode == m)
    return;

    external_point = Point();

    // save current mode for after transition:
    if (m == MODE_DROP && mode != MODE_DROP)
    old_mode = mode;

    // if manually leaving drop mode, forget about
    // restoring the previous mode when the drop
    // expires...
    else if (m != MODE_DROP && mode == MODE_DROP)
    old_mode = MODE_NONE;

    if (m == MODE_VIRTUAL && ship && !ship->Cockpit())
    return;

    if (mode == m) {
        if (mode == MODE_TARGET || mode == MODE_ORBIT)
        CycleViewObject();

        return;
    }

    if (m > MODE_NONE && m < MODE_LAST) {
        if (m <= MODE_VIRTUAL) {
            requested_mode = m;
            transition     = t;
            external_ship  = 0;
            ClearGroup();

            // no easy way to do a smooth transition between
            // certain modes, so just go immediately:
            if ((mode == MODE_TARGET  && m == MODE_CHASE)   ||
                    (mode == MODE_COCKPIT && m == MODE_VIRTUAL) ||
                    (mode == MODE_VIRTUAL && m == MODE_COCKPIT))
            {
                mode           = m;
                requested_mode = 0;
                transition     = 0;
            }
        }

        else if (m == MODE_TRANSLATE || m == MODE_ZOOM) {
            requested_mode = m;
            transition     = t;
            base_range     = range;
        }

        else if (m >= MODE_DOCKING) {
            mode           = m;
            transition     = 0;
            external_ship  = 0;
            ClearGroup();
        }

        virt_az = 0;
        virt_el = 0;
        virt_x  = 0;
        virt_y  = 0;
        virt_z  = 0;
    }
}

// +--------------------------------------------------------------------+

static const char* get_camera_mode_name(int m)
{
    switch (m) {
    default:
    case CameraDirector::MODE_NONE:      return "";           break;
    case CameraDirector::MODE_COCKPIT:   return "";           break;
    case CameraDirector::MODE_CHASE:     return "Chase Cam";  break;
    case CameraDirector::MODE_TARGET:    return "Padlock";    break;
    case CameraDirector::MODE_THREAT:    return "Threatlock"; break;
    case CameraDirector::MODE_VIRTUAL:   return "Virtual";    break;
    case CameraDirector::MODE_ORBIT:
    case CameraDirector::MODE_TRANSLATE:
    case CameraDirector::MODE_ZOOM:      return "Orbit Cam";  break;
    case CameraDirector::MODE_DOCKING:   return "Dock Cam";   break;
    case CameraDirector::MODE_DROP:      return "";           break;
    }
}

const char*
CameraDirector::GetModeName()
{
    int m = GetCameraMode();

    if (m != CameraDirector::MODE_VIRTUAL) {
        return get_camera_mode_name(m);
    }
    else if (instance) {
        if (instance->virt_az > 30*DEGREES && instance->virt_az < 100*DEGREES)
        return "RIGHT";

        else if (instance->virt_az >= 100*DEGREES)
        return "RIGHT-AFT";

        else if (instance->virt_az < -30*DEGREES && instance->virt_az > -100*DEGREES)
        return "LEFT";

        else if (instance->virt_az <= -100*DEGREES)
        return "LEFT-AFT";

        else if (instance->virt_el > 15*DEGREES)
        return "UP";

        else if (instance->virt_el < -15*DEGREES)
        return "DOWN";

        return get_camera_mode_name(m);
    }

    return "";
}

int
CameraDirector::GetCameraMode()
{
    if (instance) {
        int op_mode = instance->mode;
        if (instance->requested_mode > instance->mode)
        op_mode = instance->requested_mode;
        return op_mode;
    }

    return 0;
}

void
CameraDirector::SetCameraMode(int m, double t)
{
    if (instance)
    instance->SetMode(m, t);
}

// +--------------------------------------------------------------------+

double
CameraDirector::GetRangeLimit()
{
    return range_max_limit;
}

void
CameraDirector::SetRangeLimit(double r)
{
    if (r >= 1e3)
    range_max_limit = r;
}

void
CameraDirector::SetRangeLimits(double min, double max)
{
    if (instance) {
        instance->range_min = min;
        instance->range_max = max;
    }
}

// +--------------------------------------------------------------------+

void
CameraDirector::ClearGroup()
{
    external_group.clear();
}

// +--------------------------------------------------------------------+

void
CameraDirector::CycleViewObject()
{
    if (!ship) return;

    Ship* current = external_ship;
    external_ship  = 0;

    ListIter<Contact> iter = ship->ContactList();
    while (++iter && !external_ship) {
        Contact* c = iter.value();
        Ship*    c_ship = c->GetShip();

        if (c_ship && !current) {
            external_ship = c_ship;
        }
        
        else if (current && c_ship == current) {
            while (++iter && !external_ship) {
                c = iter.value();
                if (c->ActLock())
                external_ship = c->GetShip();
            }
        }
    }

    if (external_ship != current) {
        if (external_ship) {
            if (external_ship->Life() == 0 || external_ship->IsDying() || external_ship->IsDead()) {
                external_point = external_ship->Location();
                external_ship = 0;
            }
            else {
                Observe(external_ship);
            }
        }

        if (mode == MODE_ORBIT) {
            SetMode(MODE_TRANSLATE);
            ExternalRange(1);
        }
    }
}

// +--------------------------------------------------------------------+

void
CameraDirector::SetViewOrbital(Orbital* orb)
{
    external_body = orb;

    if (external_body) {
        range_min = external_body->Radius() * 2.5;
        ClearGroup();
        external_ship = 0;

        if (sim) {
            region = sim->FindNearestSpaceRegion(orb);
            if (region)
            sim->ActivateRegion(region);
        }

        if (ship && !region)
        region = ship->GetRegion();
    }
}

// +--------------------------------------------------------------------+

void
CameraDirector::SetViewObject(Ship* obj, bool quick)
{
    if (!ship) return;
    if (!obj) {
        obj = ship;
        region = ship->GetRegion();
    }

    external_body  = 0;
    external_point = Point();

    Starshatter* stars = Starshatter::GetInstance();

    if (obj->GetIFF() != ship->GetIFF() && !stars->InCutscene()) {
        // only view solid contacts:
        Contact* c = ship->FindContact(obj);
        if (!c || !c->ActLock()) 
        return;
    }

    if (mode == MODE_TARGET) {
        ClearGroup();
        if (external_ship) {
            external_ship = 0;
        }
    }
    else if (mode >= MODE_ORBIT) {
        if (quick) {
            mode       = MODE_ORBIT;
            transition = 0;
        }
        else {
            SetMode(MODE_TRANSLATE);
        }

        if (external_group.size()) {
            ClearGroup();

            if (external_ship) {
                external_ship = 0;
            }
        }

        else {
            if ((obj == external_ship) || (obj==ship && external_ship==0)) {
                if (!quick)
                SetMode(MODE_ZOOM);
            }
            
            else if (external_ship) {
                external_ship = 0;
            }
        }
    }

    if (external_ship != obj) {
        external_ship = obj;

        if (external_ship) {
            region = external_ship->GetRegion();

            if (external_ship->Life() == 0 || external_ship->IsDying() || external_ship->IsDead()) {
                external_ship = 0;
                range_min = 100;
            }
            else {
                Observe(external_ship);

                if (sim)
                sim->ActivateRegion(external_ship->GetRegion());

                range_min = external_ship->Radius() * 1.5;
            }
        }

        Observe(external_ship);
        ExternalRange(1);
    }
}

// +--------------------------------------------------------------------+

void
CameraDirector::SetViewObjectGroup(ListIter<Ship> group, bool quick)
{
    if (!ship) return;

    Starshatter* stars = Starshatter::GetInstance();

    if (!stars->InCutscene()) {
        // only view solid contacts:
        while (++group) {
            Ship* s = group.value();

            if (s->GetIFF() != ship->GetIFF()) {
                Contact* c = ship->FindContact(s);
                if (!c || !c->ActLock())
                return;
            }

            if (s->Life() == 0 || s->IsDying() || s->IsDead())
            return;
        }
    }

    group.reset();

    if (external_group.size() > 1 &&
            external_group.size() == group.size()) {

        bool same = true;

        for (int i = 0; same && i < external_group.size(); i++) {
            if (external_group[i] != group.container()[i])
            same = false;
        }

        if (same) {
            SetMode(MODE_ZOOM);
            return;
        }
    }

    ClearGroup();

    if (quick) {
        mode = MODE_ORBIT;
        transition = 0;
    }
    else {
        SetMode(MODE_TRANSLATE);
    }

    external_group.append(group.container());

    ListIter<Ship> iter = external_group;
    while (++iter) {
        Ship* s = iter.value();
        region = s->GetRegion();
        Observe(s);
    }
}

// +--------------------------------------------------------------------+

void
CameraDirector::VirtualHead(double az, double el)
{
    if (mode == MODE_VIRTUAL || mode == MODE_TARGET || mode == MODE_COCKPIT) {
        const double alimit = 3*PI/4;
        const double e_lo   = PI/8;
        const double e_hi   = PI/3;
        const double escale = e_hi;

        virt_az = az * alimit;
        virt_el = el * escale;

        if (virt_az > alimit)
        virt_az = alimit;

        else if (virt_az < -alimit)
        virt_az = -alimit;

        if (virt_el > e_hi)
        virt_el = e_hi;

        else if (virt_el < -e_lo)
        virt_el = -e_lo;
    }
}

void
CameraDirector::VirtualHeadOffset(double x, double y, double z)
{
    if (mode == MODE_VIRTUAL || mode == MODE_TARGET || mode == MODE_COCKPIT) {
        virt_x = x;
        virt_y = y;
        virt_z = z;
    }
}

void
CameraDirector::VirtualAzimuth(double delta)
{
    if (mode == MODE_VIRTUAL || mode == MODE_TARGET || mode == MODE_COCKPIT) {
        virt_az += delta;

        const double alimit = 3*PI/4;

        if (virt_az > alimit)
        virt_az = alimit;

        else if (virt_az < -alimit)
        virt_az = -alimit;
    }
}

void
CameraDirector::VirtualElevation(double delta)
{
    if (mode == MODE_VIRTUAL || mode == MODE_TARGET || mode == MODE_COCKPIT) {
        virt_el += delta;

        const double e_lo = PI/8;
        const double e_hi = PI/3;

        if (virt_el > e_hi)
        virt_el = e_hi;

        else if (virt_el < -e_lo)
        virt_el = -e_lo;
    }
}

// +--------------------------------------------------------------------+

void
CameraDirector::ExternalAzimuth(double delta)
{
    azimuth += delta;

    if (azimuth > PI)
    azimuth = -2*PI + azimuth;

    else if (azimuth < -PI)
    azimuth = 2*PI + azimuth;
}

void
CameraDirector::ExternalElevation(double delta)
{
    elevation += delta;

    const double limit = (0.43 * PI);

    if (elevation > limit)
    elevation = limit;
    else if (elevation < -limit)
    elevation = -limit;
}

void
CameraDirector::ExternalRange(double delta)
{
    range *= delta;

    if (ship && ship->IsAirborne())
    range_max = 30e3;
    else
    range_max = range_max_limit;

    if (range < range_min)
    range = range_min;

    else if (range > range_max)
    range = range_max;
}

// +--------------------------------------------------------------------+

void
CameraDirector::SetOrbitPoint(double a, double e, double r)
{
    azimuth     = a;
    elevation   = e;
    range       = r;

    if (external_body) {
        if (range < external_body->Radius() * 2)
        range = external_body->Radius() * 2;

        else if (range > external_body->Radius() * 6)
        range = external_body->Radius() * 6;
    }

    else {
        if (range < range_min)
        range = range_min;

        else if (range > range_max)
        range = range_max;
    }
}

void
CameraDirector::SetOrbitRates(double ar, double er, double rr)
{
    az_rate = ar;
    el_rate = er;

    range_rate = rr;
}

// +--------------------------------------------------------------------+

bool
CameraDirector::Update(SimObject* obj)
{
    if (obj->Type() == SimObject::SIM_SHIP) {
        Ship* s = (Ship*) obj;
        if (ship == s)
        ship = 0;

        if (external_ship == s) {
            external_point = s->Location();
            external_ship = 0;
        }

        if (external_group.contains(s))
        external_group.remove(s);
    }

    return SimObserver::Update(obj);
}

const char*
CameraDirector::GetObserverName() const
{
    return "CameraDirector";
}

// +--------------------------------------------------------------------+

void
CameraDirector::ExecFrame(double seconds)
{
    if (!ship)
    return;

    hud = HUDView::GetInstance();

    int flight_phase = ship->GetFlightPhase();

    if (flight_phase < Ship::LOCKED)
    SetMode(MODE_DOCKING);

    if (ship->IsAirborne()) {
        if (flight_phase >= Ship::DOCKING)
        SetMode(MODE_DOCKING);
    }
    else {
        if (flight_phase >= Ship::RECOVERY)
        SetMode(MODE_DOCKING);
    }

    if (flight_phase >= Ship::LOCKED && flight_phase < Ship::ACTIVE) {
        int m = GetMode();
        if (m != MODE_COCKPIT && m != MODE_VIRTUAL)
        SetMode(MODE_COCKPIT);
    }

    if (ship->InTransition()) {
        SetMode(MODE_DROP);
    }
    // automatically restore mode after transition:
    else if (old_mode != MODE_NONE) {
        mode           = old_mode;
        requested_mode = old_mode;
        old_mode       = MODE_NONE;
    }

    int op_mode = mode;
    if (requested_mode > mode)
    op_mode = requested_mode;

    Starshatter* stars = Starshatter::GetInstance();

    // if we are in padlock, and have not locked a ship
    // try to padlock the current target:
    if (op_mode == MODE_TARGET && !external_ship) {
        SimObject* tgt = ship->GetTarget();
        if (tgt && tgt->Type() == SimObject::SIM_SHIP)
        SetViewObject((Ship*) tgt);
    }

    // if in an external mode, check the external ship:
    else if (op_mode >= MODE_TARGET && op_mode <= MODE_ZOOM) {
        if (external_ship && external_ship != ship && !stars->InCutscene()) {
            Contact* c = ship->FindContact(external_ship);
            if (!c || !c->ActLock()) {
                SetViewObject(ship);
            }
        }
    }

    if (ship->Rep()) {
        if (op_mode == MODE_COCKPIT) {
            ship->HideRep();
            ship->HideCockpit();
        }
        else if (op_mode == MODE_VIRTUAL || op_mode == MODE_TARGET) {
            if (ship->Cockpit()) {
                ship->HideRep();
                ship->ShowCockpit();
            }
            else {
                ship->ShowRep();
            }
        }
        else {
            ship->Rep()->SetForeground(op_mode == MODE_DOCKING);
            ship->ShowRep();
            ship->HideCockpit();
        }
    }

    if (hud && hud->Ambient() != Color::Black)
    sim->GetScene()->SetAmbient( hud->Ambient() );
    else
    sim->GetScene()->SetAmbient( sim->GetStarSystem()->Ambient() );

    switch (op_mode) {
    default:
    case MODE_COCKPIT:      Cockpit(seconds);    break;
    case MODE_CHASE:        Chase(seconds);      break;
    case MODE_TARGET:       Target(seconds);     break;
    case MODE_THREAT:       Threat(seconds);     break;
    case MODE_VIRTUAL:      Virtual(seconds);    break;
    case MODE_ORBIT:
    case MODE_TRANSLATE:
    case MODE_ZOOM:         Orbit(seconds);      break;
    case MODE_DOCKING:      Docking(seconds);    break;
    case MODE_DROP:         Drop(seconds);       break;
    }

    if (ship->Shake() > 0 && 
            (op_mode <  MODE_ORBIT ||
                (op_mode == MODE_VIRTUAL && ship->Cockpit()))) {

        Point vib = ship->Vibration() * 0.2;
        camera.MoveBy(vib);
        camera.Aim(0, vib.y, vib.z);
    }

    Transition(seconds);
    DetailSet::SetReference(region, camera.Pos());
}

// +--------------------------------------------------------------------+

void
CameraDirector::Transition(double seconds)
{
    if (transition > 0)
    transition -= seconds * 1.5;

    if (transition <= 0) {
        transition = 0;

        if (requested_mode && requested_mode != mode)
        mode = requested_mode;

        requested_mode = 0;

        if (mode == MODE_TRANSLATE || mode == MODE_ZOOM) {
            if (mode ==  MODE_ZOOM)
            range = range_min;

            mode = MODE_ORBIT;
        }
    }
}

// +--------------------------------------------------------------------+

bool
CameraDirector::IsViewCentered()
{
    if (instance) {
        double fvaz = fabs(instance->virt_az);
        double fvel = fabs(instance->virt_el);

        return fvaz < 15*DEGREES && fvel < 15*DEGREES;
    }

    return true;
}

// +--------------------------------------------------------------------+

void
CameraDirector::Cockpit(double seconds)
{
    camera.Clone(ship->Cam());

    Point bridge = ship->BridgeLocation();
    Point cpos   = camera.Pos()            + 
    camera.vrt() * bridge.x +
    camera.vpn() * bridge.y +
    camera.vup() * bridge.z;

    camera.MoveTo(cpos);
}

// +--------------------------------------------------------------------+

void
CameraDirector::Virtual(double seconds)
{
    camera.Clone(ship->Cam());

    Point bridge = ship->BridgeLocation();
    Point cpos   = camera.Pos()            + 
    camera.vrt() * (bridge.x + virt_x) +
    camera.vpn() * (bridge.y + virt_z) +
    camera.vup() * (bridge.z + virt_y);

    camera.MoveTo(cpos);

    camera.Yaw(virt_az);
    camera.Pitch(-virt_el);

    double fvaz = fabs(virt_az);
    double fvel = fabs(virt_el);

    if (fvaz > 0.01*DEGREES && fvaz < 15*DEGREES) {
        double bleed = fvaz * 2;

        if (virt_az > 0)
        virt_az -= bleed * seconds;
        else
        virt_az += bleed * seconds;
    }

    if (fvel > 0.01*DEGREES && fvel < 15*DEGREES) {
        double bleed = fvel;

        if (virt_el > 0)
        virt_el -= bleed * seconds;
        else
        virt_el += bleed * seconds;
    }
}

// +--------------------------------------------------------------------+

void
CameraDirector::Chase(double seconds)
{
    double step  = 1;

    if (requested_mode == MODE_COCKPIT)
    step = transition;

    else if (requested_mode == MODE_CHASE)
    step = 1 - transition;

    camera.Clone(ship->Cam());
    Point velocity = camera.vpn();

    if (ship->Velocity().length() > 10) {
        velocity = ship->Velocity();
        velocity.Normalize();
        velocity *= 0.25;
        velocity += camera.vpn() * 2;
        velocity.Normalize();
    }

    Point chase  = ship->ChaseLocation();
    Point bridge = ship->BridgeLocation();
    Point cpos   = camera.Pos()                        + 
    camera.vrt() * bridge.x * (1-step)  +
    camera.vpn() * bridge.y * (1-step)  +
    camera.vup() * bridge.z * (1-step)  +
    velocity     * chase.y  * step      + 
    camera.vup() * chase.z  * step;

    camera.MoveTo(cpos);
}

// +--------------------------------------------------------------------+

void
CameraDirector::Target(double seconds)
{
    Point target_loc = external_point;

    if (external_ship)
    target_loc = external_ship->Location();

    if (!external_ship || external_ship == ship) {
        if (!external_point) {
            if (ship->Cockpit())
            Virtual(seconds);
            else
            Orbit(seconds);

            return;
        }
    }

    double step  = 1;

    if (requested_mode == MODE_COCKPIT)
    step = transition;

    else if (requested_mode == MODE_TARGET)
    step = 1 - transition;

    if (ship->Cockpit()) {
        // internal padlock:
        Cockpit(seconds);
        camera.Padlock(target_loc, 3*PI/4, PI/8, PI/3);
    }
    else {
        // external padlock:
        Point delta = target_loc - ship->Location();
        delta.Normalize();
        delta *= -5 * ship->Radius() * step;
        delta.y += ship->Radius() * step; 

        camera.MoveTo(ship->Location() + delta);
        camera.LookAt(target_loc);
    }
}

// +--------------------------------------------------------------------+

void
CameraDirector::Threat(double seconds)
{
    Chase(seconds);
}

// +--------------------------------------------------------------------+

void
CameraDirector::Orbit(double seconds)
{
    Point cpos    = ship->Location();
    int   op_mode = GetCameraMode();

    if (seconds < 0)        seconds = 0;
    else if (seconds > 0.2) seconds = 0.2;

    // auto rotate
    azimuth   += az_rate * seconds;
    elevation += el_rate * seconds;
    range     *= 1 + range_rate * seconds;

    if (external_body && external_body->Rep()) {
        range_min = external_body->Radius() * 2.5;
        cpos      = external_body->Rep()->Location();
    }

    else if (external_group.size()) {
        Point neg(1e9, 1e9, 1e9);
        Point pos(-1e9, -1e9, -1e9);

        ListIter<Ship> iter(external_group);
        while (++iter) {
            Point loc = iter->Location();

            if (loc.x < neg.x) neg.x = loc.x;
            if (loc.x > pos.x) pos.x = loc.x;
            if (loc.y < neg.y) neg.y = loc.y;
            if (loc.y > pos.y) pos.y = loc.y;
            if (loc.z < neg.z) neg.z = loc.z;
            if (loc.z > pos.z) pos.z = loc.z;
        }

        double dx = pos.x - neg.x;
        double dy = pos.y - neg.y;
        double dz = pos.z - neg.z;

        if (dx > dy) {
            if (dx > dz)   range_min = dx * 1.2;
            else           range_min = dz * 1.2;
        }
        else {
            if (dy > dz)   range_min = dy * 1.2;
            else           range_min = dz * 1.2;
        }

        // focus on median location:
        cpos = neg + Point(dx/2, dy/2, dz/2);
    }
    else if (external_ship) {
        range_min = external_ship->Radius() * 1.5;
        cpos      = external_ship->Location();
    }
    else {
        range_min = ship->Radius() * 1.5;
        cpos      = ship->Location();
    }

    if (range < range_min)
    range = range_min;

    double er = range;
    double az = azimuth;
    double el = elevation;

    if (requested_mode == MODE_TRANSLATE) {
        cpos = cpos*(1-transition) + base_loc*(transition);
    }

    else if (requested_mode == MODE_ZOOM) {
        er = base_range * transition;

        if (er < range_min) {
            er             = range_min;
            range          = range_min;
            transition     = 0;
            requested_mode = 0;
        }
    }

    // transitions:
    else if (mode < MODE_ORBIT || requested_mode > 0 && requested_mode < MODE_ORBIT) {
        double az0 = ship->CompassHeading();
        if (fabs(az-az0) > PI) az0 -= 2*PI;

        double r0 = 0;
        double z0 = 0;

        if (mode == MODE_CHASE  || requested_mode == MODE_CHASE ||
                mode == MODE_TARGET || requested_mode == MODE_TARGET) {
            r0 = ship->ChaseLocation().length();
            z0 = 20 * DEGREES;
        }

        // pull out:
        if (mode < MODE_ORBIT) {
            er *= (1-transition);
            az *= (1-transition);
            el *= (1-transition);

            er += r0  * transition;
            az += az0 * transition;
            el += z0  * transition;
        }

        // push in:
        else {
            er *= transition;
            az *= transition;
            el *= transition;

            er += r0  * (1-transition);
            az += az0 * (1-transition);
            el += z0  * (1-transition);
        }
    }

    else {
        // save base location for next time we re-focus
        base_loc = cpos;
    }

    double dx = er * sin(az) * cos(el);
    double dy = er * cos(az) * cos(el);
    double dz = er *           sin(el);

    Point cloc = cpos + Point(dx,dz,dy);

    Terrain* terrain = ship->GetRegion()->GetTerrain();

    if (terrain) {
        double cam_agl = cloc.y - terrain->Height(cloc.x, cloc.z);

        if (cam_agl < 100)
        cloc.y = terrain->Height(cloc.x, cloc.z) + 100;
    }

    if (external_ship == 0 && er < 0.5 * ship->Radius())
    ship->Rep()->Hide();

    camera.MoveTo(cloc.x, cloc.y, cloc.z);
    camera.LookAt(cpos);
}

// +--------------------------------------------------------------------+

void
CameraDirector::Docking(double seconds)
{
    FlightDeck* dock = ship->GetDock();

    if (!dock) {
        Cockpit(seconds);
        return;
    }

    if (!ship->IsAirborne())
    sim->GetScene()->SetAmbient(Color(120,130,140));
    int flight_phase = ship->GetFlightPhase();

    Point bridge = ship->BridgeLocation();
    Point cloc   = ship->Location()                    +
    ship->Cam().vrt() * bridge.x        +
    ship->Cam().vpn() * bridge.y        +
    ship->Cam().vup() * bridge.z;

    Point cpos   = dock->CamLoc();

    // preflight:
    if (flight_phase < Ship::LOCKED) {
        base_loc = cpos;
    }

    else if (flight_phase == Ship::LOCKED) {
        if (hud)
        hud->SetHUDMode(HUDView::HUD_MODE_TAC);
        cpos = base_loc * transition +
        cloc     * (1-transition);
    }

    // recovery:
    else if (flight_phase > Ship::APPROACH) {
        if (hud)
        hud->SetTacticalMode(1);
    }

    camera.MoveTo(cpos);
    camera.LookAt(cloc);
}

// +--------------------------------------------------------------------+

void
CameraDirector::Drop(double seconds)
{
    // orbital transitions use "drop cam" at transition_loc
    camera.MoveTo(ship->TransitionLocation());
    camera.LookAt(ship->Location());
}
